/*
 * Copyright 2021 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package androidx.compose.foundation.lazy.list

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.CollectionInfo
import androidx.compose.ui.semantics.SemanticsActions.ScrollToIndex
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.semantics.SemanticsProperties
import androidx.compose.ui.semantics.SemanticsProperties.IndexForKey
import androidx.compose.ui.semantics.getOrNull
import androidx.compose.ui.test.SemanticsMatcher
import androidx.compose.ui.test.assert
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performClick
import androidx.compose.ui.unit.dp
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.MediumTest
import junit.framework.TestCase.assertEquals
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith

/**
 * Tests the semantics properties defined on a LazyList:
 * - GetIndexForKey
 * - ScrollToIndex
 *
 * GetIndexForKey: Create a lazy list, iterate over all indices, verify key of each of them
 *
 * ScrollToIndex: Create a lazy list, scroll to an item off screen, verify shown items
 *
 * All tests performed in [runTest], scenarios set up in the test methods.
 */
@MediumTest
@RunWith(AndroidJUnit4::class)
class LazySemanticsTest {
    private val N = 20
    private val LazyListTag = "lazy_list"
    private val BasicTextTag = "basic_text"
    private val LazyListModifier = Modifier.testTag(LazyListTag).requiredSize(100.dp)
    private val BasicTextModifier = Modifier.testTag(BasicTextTag)

    private fun tag(index: Int): String = "tag_$index"

    private fun key(index: Int): String = "key_$index"

    @get:Rule val rule = createComposeRule()

    @Test
    fun itemSemantics_column() {
        rule.setContent {
            LazyColumn(LazyListModifier) {
                repeat(N) { item(key = key(it)) { SpacerInColumn(it) } }
            }
        }
        runTest()
    }

    @Test
    fun itemsSemantics_column() {
        rule.setContent {
            LazyColumn(LazyListModifier) {
                items(items = List(N) { it }, key = { key(it) }) { SpacerInColumn(it) }
            }
        }
        runTest()
    }

    @Test
    fun itemSemantics_row() {
        rule.setContent {
            LazyRow(LazyListModifier) { repeat(N) { item(key = key(it)) { SpacerInRow(it) } } }
        }
        runTest()
    }

    @Test
    fun itemsSemantics_row() {
        rule.setContent {
            LazyRow(LazyListModifier) {
                items(items = List(N) { it }, key = { key(it) }) { SpacerInRow(it) }
            }
        }
        runTest()
    }

    @Test
    fun hasCorrectCollectionInfoInLazyColumn() {
        rule.setContent { LazyColumn(LazyListModifier) { items(N) { SpacerInColumn(it) } } }
        assertEquals(N, fetchCollectionInfo().rowCount)
    }

    @Test
    fun hasCorrectCollectionInfoInLazyRow() {
        rule.setContent { LazyRow(LazyListModifier) { items(N) { SpacerInRow(it) } } }
        assertEquals(N, fetchCollectionInfo().columnCount)
    }

    @Test
    fun hasCorrectCollectionInfoAfterLazyColumnItemsUpdate() {
        rule.setContent {
            var itemCount by remember { mutableStateOf(N) }
            LazyColumn(LazyListModifier) { items(itemCount) { SpacerInColumn(it) } }
            BasicTextWithClick { itemCount *= 2 }
        }
        assertEquals(N, fetchCollectionInfo().rowCount)
        rule.onNodeWithTag(BasicTextTag).performClick()
        assertEquals(2 * N, fetchCollectionInfo().rowCount)
    }

    @Test
    fun hasCorrectCollectionInfoAfterLazyRowItemsUpdate() {
        rule.setContent {
            var itemCount by remember { mutableStateOf(N) }
            LazyRow(LazyListModifier) { items(itemCount) { SpacerInRow(it) } }
            BasicTextWithClick { itemCount *= 2 }
        }
        assertEquals(N, fetchCollectionInfo().columnCount)
        rule.onNodeWithTag(BasicTextTag).performClick()
        assertEquals(2 * N, fetchCollectionInfo().columnCount)
    }

    private fun runTest() {
        checkViewport(firstExpectedItem = 0, lastExpectedItem = 3)

        // Verify IndexForKey
        rule
            .onNodeWithTag(LazyListTag)
            .assert(
                SemanticsMatcher.keyIsDefined(IndexForKey)
                    .and(
                        SemanticsMatcher("keys match") { node ->
                            val actualIndex = node.config.getOrNull(IndexForKey)!!
                            (0 until N).all { expectedIndex ->
                                expectedIndex == actualIndex.invoke(key(expectedIndex))
                            }
                        }
                    )
            )

        // Verify ScrollToIndex
        rule.onNodeWithTag(LazyListTag).assert(SemanticsMatcher.keyIsDefined(ScrollToIndex))

        invokeScrollToIndex(targetIndex = 10)
        checkViewport(firstExpectedItem = 10, lastExpectedItem = 13)

        invokeScrollToIndex(targetIndex = N - 1)
        checkViewport(firstExpectedItem = N - 4, lastExpectedItem = N - 1)
    }

    private fun invokeScrollToIndex(targetIndex: Int) {
        val node =
            rule.onNodeWithTag(LazyListTag).fetchSemanticsNode("Failed: invoke ScrollToIndex")
        rule.runOnUiThread { node.config[ScrollToIndex].action!!.invoke(targetIndex) }
    }

    private fun checkViewport(firstExpectedItem: Int, lastExpectedItem: Int) {
        if (firstExpectedItem > 0) {
            rule.onNodeWithTag(tag(firstExpectedItem - 1)).assertDoesNotExist()
        }
        (firstExpectedItem..lastExpectedItem).forEach { rule.onNodeWithTag(tag(it)).assertExists() }
        if (firstExpectedItem < N - 1) {
            rule.onNodeWithTag(tag(lastExpectedItem + 1)).assertDoesNotExist()
        }
    }

    private fun fetchCollectionInfo(testTag: String = LazyListTag): CollectionInfo =
        fetchSemanticNode(tag = testTag).config.getOrElse(SemanticsProperties.CollectionInfo) {
            throw AssertionError("Node with $testTag tag does not have CollectionInfo semantics.")
        }

    private fun fetchSemanticNode(tag: String): SemanticsNode =
        rule.onNodeWithTag(tag).fetchSemanticsNode("Could not find node with $tag tag.")

    @Composable
    private fun SpacerInColumn(index: Int) {
        Spacer(Modifier.testTag(tag(index)).requiredHeight(30.dp).fillMaxWidth())
    }

    @Composable
    private fun SpacerInRow(index: Int) {
        Spacer(Modifier.testTag(tag(index)).requiredWidth(30.dp).fillMaxHeight())
    }

    @Composable
    private fun BasicTextWithClick(onClick: () -> Unit) {
        BasicText(text = "basic text", modifier = BasicTextModifier.clickable(onClick = onClick))
    }
}
